/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.uml_generator.manager.impl;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.genia.toolbox.basics.exception.BundledException;
import com.genia.toolbox.basics.manager.ExceptionManager;
import com.genia.toolbox.basics.manager.FileManager;
import com.genia.toolbox.basics.manager.StreamManager;
import com.genia.toolbox.basics.manager.XmlManager;
import com.genia.toolbox.uml_generator.bean.GenerationConfiguration;
import com.genia.toolbox.uml_generator.bean.ModelGeneratorContext;
import com.genia.toolbox.uml_generator.manager.ModelGenerator;
import com.genia.toolbox.uml_generator.manager.UmlGeneratorManager;
import com.genia.toolbox.uml_generator.transformer.InputHandler;

/**
 * implementation of {@link UmlGeneratorManager}.
 */
public abstract class AbstractUmlGeneratorManagerImpl
    implements UmlGeneratorManager
{

  /**
   * the <code>Log</code> variabe.
   */
  private static final Logger LOGGER = LoggerFactory.getLogger(AbstractUmlGeneratorManagerImpl.class);

  /**
   * the attribute idref of {@link Element}.
   */
  private static final String XMI_IDREF = "xmi.idref";

  /**
   * the attribute id of {@link Element}.
   */
  private static final String XMI_ID = "xmi.id";

  /**
   * the {@link StreamManager} to handle streams.
   */
  private StreamManager streamManager;

  /**
   * the {@link XmlManager} to use.
   */
  private XmlManager xmlManager;

  /**
   * the {@link ModelGenerator} to use.
   */
  private ModelGenerator modelGenerator;

  /**
   * the {@link InputHandler} to use to parse the xmi file.
   */
  private InputHandler<Object> inputHandlerDispatcher;

  /**
   * the {@link ExceptionManager} to use.
   */
  private ExceptionManager exceptionManager;

  /**
   * the {@link FileManager} to use.
   */
  private FileManager fileManager;



  /**
   * the {@link ModelGeneratorContext} factory.
   * 
   * @return a new {@link ModelGeneratorContext}
   */
  public abstract ModelGeneratorContext createContext();



  /**
   * generate the java class from the UML modelisation.
   * 
   * @param configuration
   *          the configuration of the generation
   * @throws BundledException
   *           when an error occurred
   * @see com.genia.toolbox.uml_generator.manager.UmlGeneratorManager#generateModele(com.genia.toolbox.uml_generator.bean.GenerationConfiguration)
   */
  public void generateModele(final GenerationConfiguration configuration)
      throws BundledException
  {
    try {
      ModelGeneratorContext context = createContext();
      context.setConfiguration(configuration);

      for (File file : configuration.getXmiFiles()) {
        Document document = getXmlManager().parse(file);
        context.addElements(getXmlManager().getAllSubElements(document.getDocumentElement()));
        getInputHandlerDispatcher().processContent(context, true, document.getDocumentElement(), null);
        getInputHandlerDispatcher().processContent(context, false, document.getDocumentElement(), null);
      }
      if (!context.getErrors().isEmpty()) {
        StringBuffer errorsBuffer = new StringBuffer();
        errorsBuffer.append("Error generating model:\n");
        for (String error : context.getErrors()) {
          LOGGER.error(error);
          errorsBuffer.append(error).append("\n");
        }
        throw new IOException(errorsBuffer.toString());
      }
      getModelGenerator().generate(context);
    }
    catch (IOException e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * return a {@link File} that is an UML model with all element tagged with
   * ignore tag.
   * 
   * @param model
   *          the model to transform
   * @return the transformed model
   * @throws BundledException
   *           when an error occurred
   * @see com.genia.toolbox.uml_generator.manager.UmlGeneratorManager#generateTemplateFromModele(java.io.File)
   */
  public File generateTemplateFromModele(final File model)
      throws BundledException
  {
    File xmiFile = getXmiFile(model);

    Document document = getXmlManager().parse(xmiFile);

    transformIds(document);
    Element ignoreStereotypeElement = getIgnoredStereotypeElement(document);

    for (String tagName : Arrays.asList("UML:Class", "UML:Interface", "UML:Enumeration")) {
      for (Element element : getXmlManager().getElements(document.getElementsByTagName(tagName))) {
        addIgnoreTag(document, element, getId(ignoreStereotypeElement));
      }
    }

    File resFile = getFileManager().createAutoDeletableTempFile("xmiTemplateFile", ".xmi");
    getXmlManager().print(document, resFile);

    return resFile;
  }



  /**
   * getter for the inputHandlerDispatcher property.
   * 
   * @return the inputHandlerDispatcher
   */
  public InputHandler<Object> getInputHandlerDispatcher()
  {
    return inputHandlerDispatcher;
  }



  /**
   * getter for the modelGenerator property.
   * 
   * @return the modelGenerator
   */
  public ModelGenerator getModelGenerator()
  {
    return modelGenerator;
  }



  /**
   * getter for the streamManager property.
   * 
   * @return the streamManager
   */
  public StreamManager getStreamManager()
  {
    return streamManager;
  }



  /**
   * returns a xmi {@link File} extracted from a zargo {@link File}.
   * 
   * @param argoFile
   *          the UML modele from Argo UML.
   * @return the xmi {@link File} corresponding to the modele.
   * @throws BundledException
   *           when an error occurred
   * @see com.genia.toolbox.uml_generator.manager.UmlGeneratorManager#getXmiFileFromArgoFile(java.io.File)
   */
  public File getXmiFileFromArgoFile(final File argoFile)
      throws BundledException
  {
    try {
      final File res = getFileManager().createAutoDeletableTempFile("zargo2xmi", ".xmi");
      final ZipFile zipFile = new ZipFile(argoFile);

      final Enumeration<? extends ZipEntry> zipEnumeration = zipFile.entries();
      while (zipEnumeration.hasMoreElements()) {
        final ZipEntry zipEntry = zipEnumeration.nextElement();
        if (zipEntry.getName().endsWith(".xmi")) {
          getStreamManager().copyStream(zipFile.getInputStream(zipEntry), new FileOutputStream(res));
          return res;
        }
      }
      return null;
    }
    catch (final IOException e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * getter for the xmlManager property.
   * 
   * @return the xmlManager
   */
  public XmlManager getXmlManager()
  {
    return xmlManager;
  }



  /**
   * merge two models.
   * 
   * @param originalModel
   *          the model that will import the other model
   * @param mergedModel
   *          the model being imported
   * @return the merged model
   * @throws BundledException
   *           when an error occurred
   * @see com.genia.toolbox.uml_generator.manager.UmlGeneratorManager#mergeFile(java.io.File,
   *      java.io.File)
   */
  public File mergeFile(final File originalModel, final File mergedModel)
      throws BundledException
  {
    Document originalDocument = getXmlManager().parse(getXmiFile(originalModel));
    Document mergedDocument = getXmlManager().parse(getXmiFile(mergedModel));
    removeOldElement(originalDocument, mergedDocument);

    Element rootOriginal = getXmiFunctionnalRoot(originalDocument);
    Element rootMerged = getXmiFunctionnalRoot(mergedDocument);
    for (Element element : getXmlManager().getElements(rootMerged.getChildNodes())) {
      rootOriginal.appendChild(originalDocument.importNode(element, true));
    }
    File resFile = getFileManager().createAutoDeletableTempFile("xmiMergedFile", ".xmi");
    getXmlManager().print(originalDocument, resFile);

    return resFile;
  }



  /**
   * setter for the inputHandlerDispatcher property.
   * 
   * @param inputHandlerDispatcher
   *          the inputHandlerDispatcher to set
   */
  public void setInputHandlerDispatcher(InputHandler<Object> inputHandlerDispatcher)
  {
    this.inputHandlerDispatcher = inputHandlerDispatcher;
  }



  /**
   * setter for the modelGenerator property.
   * 
   * @param modelGenerator
   *          the modelGenerator to set
   */
  public void setModelGenerator(ModelGenerator modelGenerator)
  {
    this.modelGenerator = modelGenerator;
  }



  /**
   * setter for the streamManager property.
   * 
   * @param streamManager
   *          the streamManager to set
   */
  public void setStreamManager(final StreamManager streamManager)
  {
    this.streamManager = streamManager;
  }



  /**
   * setter for the xmlManager property.
   * 
   * @param xmlManager
   *          the xmlManager to set
   */
  public void setXmlManager(XmlManager xmlManager)
  {
    this.xmlManager = xmlManager;
  }



  /**
   * add the ignore tag to an UML object.
   * 
   * @param document
   *          the XMI document
   * @param objectElement
   *          the object that must be tagged
   * @param ignoreStereotypeId
   *          the if of the ignore stereotype
   */
  private void addIgnoreTag(Document document, Element objectElement, String ignoreStereotypeId)
  {
    String id = getId(objectElement);
    if (id == null) {
      return;
    }
    Element stereotypeElement = getSonElement(document, objectElement, "UML:ModelElement.stereotype");
    for (Element element : getXmlManager().getElements("UML:Stereotype", stereotypeElement.getChildNodes())) {
      if (ignoreStereotypeId.equals(getId(element))) {
        return;
      }
    }
    Element newStereotypeRef = document.createElement("UML:Stereotype");
    newStereotypeRef.setAttribute(XMI_IDREF, ignoreStereotypeId);
    stereotypeElement.appendChild(newStereotypeRef);
  }



  /**
   * return the xmi.id of the given {@link Element}.
   * 
   * @param element
   *          the element being inspected
   * @return the xmi.id of the given {@link Element}
   */
  private String getId(Element element)
  {
    final String xmiId = element.getAttribute(XMI_ID);
    if (xmiId != null && !"".equals(xmiId)) {
      return xmiId;
    }
    return null;
  }



  /**
   * return the xmi.id of the given {@link Element}.
   * 
   * @param element
   *          the element being inspected
   * @return the xmi.id of the given {@link Element}
   */
  private String getIdRef(Element element)
  {
    final String xmiId = element.getAttribute(XMI_IDREF);
    if (xmiId != null && !"".equals(xmiId)) {
      return xmiId;
    }
    return null;
  }



  /**
   * returns the {@link Element} representing the ignore tag.
   * 
   * @param document
   *          the document to inspect
   * @return the {@link Element} representing the ignore tag
   */
  private Element getIgnoredStereotypeElement(Document document)
  {
    for (Element element : getXmlManager().getElements(document.getElementsByTagName("UML:Stereotype"))) {
      if ("ignore".equals(element.getAttribute("name"))) {
        return element;
      }
    }
    return null;
  }



  /**
   * returns the {@link Element} that is the son of the given parameter with the
   * given tagname. If such an element does not exist, a new one is created.
   * 
   * @param document
   *          the document the {@link Element} is from
   * @param element
   *          the element to consider
   * @param tagName
   *          the tagName of the searched {@link Element}
   * @return the {@link Element} that is the son of the given parameter with the
   *         given tagname
   */
  private Element getSonElement(Document document, Element element, String tagName)
  {
    for (Element sonElement : getXmlManager().getElements(element.getChildNodes())) {
      if (sonElement.getTagName().equals(tagName)) {
        return sonElement;
      }
    }
    Element newElement = document.createElement(tagName);
    element.appendChild(newElement);
    return newElement;
  }



  /**
   * returns the xmi {@link File} from a UML model.
   * 
   * @param modelFile
   *          the model {@link File} to consider
   * @return the xmi {@link File} from a UML model
   * @throws BundledException
   *           when an error occurred
   */
  private File getXmiFile(File modelFile)
      throws BundledException
  {
    File xmiFile = modelFile;
    if (modelFile.getName().endsWith(".zargo")) {
      xmiFile = getXmiFileFromArgoFile(modelFile);
    }
    return xmiFile;
  }



  /**
   * returns the {@link Element} of the document that is the root of all
   * functionnal elements of the UML model.
   * 
   * @param document
   *          the document to inspect
   * @return the {@link Element} of the document that is the root of all
   *         functionnal elements of the UML model
   */
  private Element getXmiFunctionnalRoot(Document document)
  {
    List<Element> uMLModelElement = getXmlManager().getElements(document.getElementsByTagName("UML:Model"));
    return getXmlManager().getElements(uMLModelElement.get(0).getChildNodes()).get(0);
  }



  /**
   * remove all the UML elements that are in mergedDocument from
   * originalDocument.
   * 
   * @param originalDocument
   *          the document to purge
   * @param mergedDocument
   *          the document containing the elements to purge
   */
  private void removeOldElement(Document originalDocument, Document mergedDocument)
  {
    Map<String, Element> elementById = new TreeMap<String, Element>();
    for (Element element : getXmlManager().getAllSubElements(originalDocument.getDocumentElement())) {
      if (getId(element) != null) {
        elementById.put(getId(element), element);
      }
    }
    for (Element element : getXmlManager().getAllSubElements(mergedDocument.getDocumentElement())) {
      String id = getId(element);
      if (id != null && elementById.containsKey(id)) {
        elementById.get(id).getParentNode().removeChild(elementById.get(id));
      }
    }
  }



  /**
   * transform all the xmi.id of a document.
   * 
   * @param document
   *          the document to transform
   */
  private void transformIds(Document document)
  {
    for (Element element : getXmlManager().getAllSubElements(document.getDocumentElement())) {
      if (getId(element) != null) {
        element.setAttribute(XMI_ID, "template-" + getId(element));
      }
      if (getIdRef(element) != null) {
        element.setAttribute(XMI_IDREF, "template-" + getIdRef(element));
      }
    }
  }



  /**
   * getter for the exceptionManager property.
   * 
   * @return the exceptionManager
   */
  public ExceptionManager getExceptionManager()
  {
    return exceptionManager;
  }



  /**
   * setter for the exceptionManager property.
   * 
   * @param exceptionManager
   *          the exceptionManager to set
   */
  public void setExceptionManager(ExceptionManager exceptionManager)
  {
    this.exceptionManager = exceptionManager;
  }



  /**
   * getter for the fileManager property.
   * 
   * @return the fileManager
   */
  public FileManager getFileManager()
  {
    return fileManager;
  }



  /**
   * setter for the fileManager property.
   * 
   * @param fileManager
   *          the fileManager to set
   */
  public void setFileManager(FileManager fileManager)
  {
    this.fileManager = fileManager;
  }

}
